package org.helios.gwt.remote.rebind;

import java.io.PrintWriter;

import org.helios.gwt.remote.client.Exporter;
import org.helios.gwt.remote.client.Importer;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;

public abstract class ExporterImporterGenerator extends Generator
{

	protected void createExporterWrapperImplType(TreeLogger logger, GeneratorContext context,
			String wrapperImplTypeName, JClassType wrappedClassType) 
	{
				PrintWriter pw = context.tryCreate(logger, EXPORTER_PACKAGE, wrapperImplTypeName);
				
				if (pw != null) // if it returns null, then the type was already created
				{
					String wrappedTypeName = wrappedClassType.getQualifiedSourceName();
					
					pw.append("package ").append(EXPORTER_PACKAGE).append(";").println();
					
					// wrapper
					pw.append("public class ").append(wrapperImplTypeName).append(" { ");
					pw.println();
					pw.append("private ").append(wrappedTypeName).append(" obj;");
					pw.println();
			
					// constructor (package protected ya que es usado desde la propia clase o desde otro exporter-wrapper)
					pw.append(wrapperImplTypeName).append("(").append(wrappedTypeName).append(" obj) {this.obj = obj;} ");
					pw.println();
					
					// methods (delegate to wrapped.method(args))
					for (JMethod m: wrappedClassType.getMethods())
					{
						appendExportWrapperMethod(logger, context, pw, m);
					}
			
					// static wrap(x) { return wrapInternal(new WrapperImpl(x)) }
					pw.append("public static final ").append(JSO_TYPENAME).append(" wrap(").append(wrappedTypeName).append(" obj) {").println();

					// check if it's an imported object, if it is, simply "unimport-it"
					String importerWrapperTypeName = createImporterWrapperImplTypeName(wrappedClassType);
					// may be the importer is not created (if this module only exports it)
					createImporterWrapperType(logger, context, importerWrapperTypeName, wrappedClassType);
					pw.append("  if (obj instanceof ").append(importerWrapperTypeName).append(") {").println();
					pw.append("      return (((").append(importerWrapperTypeName).append(") obj).obj);").println();
					// if it's not an imported object, then wrap it normally
					pw.append("   }").println();
					pw.append("   return wrapInternal(new ").append(wrapperImplTypeName).append("(obj)); }").println();
			
					// wrapInternal crea el javascript object que se devolverá al final
					pw.append("public static native final ").append(JSO_TYPENAME).append(" wrapInternal(").append(wrapperImplTypeName).append(" impl) /*-{").println();
					pw.append("   return {").println();
					
					String wTypeName = EXPORTER_PACKAGE + "." + wrapperImplTypeName;
					for (JMethod m: wrappedClassType.getMethods())
					{
						appendExportJSOMethod(logger, context, pw, wTypeName, m);
					}
					pw.append("};").println();
					pw.append("}-*/; ").println();
					
					pw.append("}");
					
					context.commit(logger, pw);
				}
			}

	protected String createExporterWrapperImplTypeName(JClassType wrappedClassType) {
		return "ExporterWrapper_" + wrappedClassType.getQualifiedSourceName().replace('.', '_');
	}

	protected static final String JSO_TYPENAME = JavaScriptObject.class.getName();
	protected static final String EXPORTER_PACKAGE = Exporter.class.getPackage().getName();
	protected static final String IMPORTER_PACKAGE = Importer.class.getPackage().getName();

	private void appendExportJSOMethod(TreeLogger logger, GeneratorContext context, PrintWriter pw, String wTypeName, JMethod m) {
		String args = "";
		String values = "";
		String argTypes = "";
		String separator = "";
		int i = 0;
		for (JParameter p: m.getParameters())
		{
			String arg = "arg" + i;
			String value = arg;

			JClassType argInterface = p.getType().isInterface();
			if (argInterface != null) // si es una interface habrá que wrappearla
			{
				String wrapperImplTypeName = createImporterWrapperImplTypeName(argInterface);
				createImporterWrapperType(logger, context, wrapperImplTypeName, argInterface);
				String jsoJsonSignature = "L" + JSO_TYPENAME.replace('.', '/') + ";";
				value = "@" + IMPORTER_PACKAGE + "." + wrapperImplTypeName +
					"::use(" + jsoJsonSignature + ")(" + value + ")";
			}
			
			args += separator + arg;
			values += separator + value;
			argTypes += getParamTypeLetter(p);
			i++;
			separator = ", ";
		}
		pw.append(m.getName()).append(": function(").append(args).append(") { ");
		
		String invocation ="impl.@" + wTypeName + "::" + m.getName() + "(" + argTypes + ")(" + values + ")";
		JClassType subInterface = m.getReturnType().isInterface();
		if (subInterface != null) // si es de tipo interface viene con la impl del wrapper y hay que convertirlo a JSON "abierto"
		{
			String wSubTypeName = EXPORTER_PACKAGE + "." + createExporterWrapperImplTypeName(subInterface);
			String wrapSignature = "L" + subInterface.getQualifiedSourceName().replace('.', '/') + ";";
			invocation = "@" + wSubTypeName + "::wrap(" + wrapSignature + ")(" + invocation + ")"; 
		}
		
		pw.append("return ").append(invocation).append(";").println();
		pw.append("},");
	}

	private String getParamTypeLetter(JParameter p) {
		return p.getType().getJNISignature();
	}

	protected String createImporterWrapperImplTypeName(JClassType wrappedClassType) {
		return  "ImporterWrapper_" + wrappedClassType.getQualifiedSourceName().replace('.', '_');
	}

	protected void createImporterWrapperType(TreeLogger logger, GeneratorContext context,
			String wrapperImplTypeName, JClassType wrappedClassType) {
				PrintWriter pw = context.tryCreate(logger, IMPORTER_PACKAGE, wrapperImplTypeName);
				
				if (pw != null) // si es null es porque ya se creó
				{
					String wrappedTypeName = wrappedClassType.getQualifiedSourceName();
			
					pw.append("package ").append(IMPORTER_PACKAGE).append(";");
					pw.println();
					
					// wrapper
					pw.append("public class ").append(wrapperImplTypeName) //.append(" extends ").append(JSO_TYPENAME)
					.append(" implements ").append(wrappedTypeName)
					.append(" { ");
					pw.println();
			
					pw.append("/*package protected*/ ").append(JSO_TYPENAME).append(" obj;");
					pw.println();
					
					// constructor (protected default)
					pw.append("protected ").append(wrapperImplTypeName).append("(").append(JSO_TYPENAME).append(" obj) { this.obj = obj; } ");
					pw.println();
					
					// methods (delegate to this.method(args))
					for (JMethod m: wrappedClassType.getMethods())
					{
						appendImporterMethod(IMPORTER_PACKAGE + "." + wrapperImplTypeName, logger, context, pw, m);
					}
					
					// use method (for wrapping a JavaScriptObject into the local Imported type)
					pw.append("public static ").append(wrapperImplTypeName).append(" use(").append(JSO_TYPENAME).append(" obj) {").println();
					pw.append("   return new ").append(wrapperImplTypeName).append("(obj);").println();
					pw.append("};");
					
					pw.append("} ");
					pw.println();
					context.commit(logger, pw);
				}
			}

	private void appendExportWrapperMethod(TreeLogger logger, GeneratorContext context, PrintWriter pw,
			JMethod m) {
				String args = "";
				String values = "";
				String separator = "";
				int i = 0;
				for (JParameter p: m.getParameters())
				{
					args += separator + p.getType().getQualifiedSourceName() + " arg" + i;
					values += separator + "arg" + i;
					i++;
					separator = ", ";
				}
				// if we have to wrap the returned type
				//JClassType subInterface = m.getReturnType().isInterface();
				//boolean isWrappedSubType = subInterface != null;
				//String wrapperImplTypeName = isWrappedSubType? "ExporterWrapper_" + subInterface.getQualifiedSourceName().replace('.', '_'): "";
				String returnType = /*isWrappedSubType? wrapperImplTypeName:*/ m.getReturnType().getQualifiedSourceName();
				pw.append("public ").append(returnType).append(" ").append(m.getName());
				String returnWord = m.getReturnType().getQualifiedSourceName().equals("void")? "": "return ";
				pw.append("(").append(args).append(") { ");
				String invocation = "obj." + m.getName() + "(" + values + ")";
				
				/*if (isWrappedSubType)
				{
					createExporterWrapperImplType(logger, context, wrapperImplTypeName, subInterface);
					invocation = "new " + wrapperImplTypeName + "(" + invocation + ")";
				}*/
				
				pw.append(returnWord).append(invocation).append("; } ");
			}

	private void appendImporterMethod(String fullClassName, TreeLogger logger, GeneratorContext context, PrintWriter pw,
			JMethod m) {
				String returnTypeName = m.getReturnType().getQualifiedSourceName();
				
				JClassType subInterface = m.getReturnType().isInterface();
				String wrapperImplTypeName = null;
				if (subInterface != null) // we must create the sub-exporter
				{
					// all the same, except we'll return a wrapper
					wrapperImplTypeName = createImporterWrapperImplTypeName(subInterface);
					createImporterWrapperType(logger, context, wrapperImplTypeName, subInterface);
					returnTypeName = wrapperImplTypeName;
				}
				
				pw.append("public final native ").append(returnTypeName).append(" ");
				pw.append(m.getName()).append("(");
				String values = "";
				String separator = "";
				int i = 0;
				for (JParameter p: m.getParameters())
				{
					pw.append(separator);
					pw.append(p.getType().getQualifiedSourceName());
					pw.append(" arg" + i);
					
					String value = "arg" + i;
					
					JClassType argInterface = p.getType().isInterface();
					if (argInterface != null) // we must export the argument (it goes in opposite way)
					{
						// asegurar exporter del tipo
						String argWrapperImplTypeName = createExporterWrapperImplTypeName(argInterface);
						createExporterWrapperImplType(logger, context, argWrapperImplTypeName, argInterface);
						// value = wrap(value) // "wrap" is the same that the base exporter does
						String wrappedJsonSignature = "L" + argInterface.getQualifiedSourceName().replace('.','/')+";";
						value = "@" + EXPORTER_PACKAGE + "." + argWrapperImplTypeName + 
							"::wrap(" + wrappedJsonSignature + ")(" + value + ")";
					}
					
					values += separator + value;
					
					i++;
					separator = ", ";
				}
				pw.append(") /*-{ ").println();
				String call = "this.@" + fullClassName + "::obj['" + m.getName() + "'](" + values + ")";
				if (subInterface != null)
				{
					String jsoJsonSignature = "L" + JSO_TYPENAME.replace('.', '/') + ";";
					call = "@" + IMPORTER_PACKAGE + "." + wrapperImplTypeName +"::use(" + jsoJsonSignature +")(" + call + ")";
				}
				pw.append("   return ").append(call).append(";").println();
				pw.append("}-*/;").println();
			}

}
